// perl_TextEditorEventSink.cpp - document event sink

#include "PlPN.h"

// don't want to pull in all of Scintilla.h for these few constants and structures
#define SCI_GETCODEPAGE   2137
#define SCI_GETCURRENTPOS 2008
#define SCI_GETCHARAT     2007
#define SCI_GETTEXTRANGE  2162

struct Sci_CharacterRange {
    long cpMin;
    long cpMax;
};

struct Sci_TextRange {
    struct Sci_CharacterRange chrg;
    char *lpstrText;
};

/*
 * PerlTextEditorEventSink
 */

PerlTextEditorEventSink::PerlTextEditorEventSink(extensions::IDocumentPtr doc, SV* perl_doc) {
	m_doc = doc;
	SvREFCNT_inc(perl_doc);
	m_perl_doc = perl_doc;
}

PerlTextEditorEventSink::~PerlTextEditorEventSink() {
	if (m_perl_doc != NULL)
		SvREFCNT_inc(m_perl_doc);
}

/*

The character is passed by Scintilla as an integer code point,
but PN (for some reason) sends it as a char, which has two
unfortunate results:

1) The top three bytes get lopped off
2) We no longer have access to original memory location of the int

So we have to get the original character ourselves, for which we use the following functions

*/

char* PerlTextEditorEventSink::GetLastAnsiCharAsUtf8(int pos) {
char last[2];
	last[0] = m_doc->SendEditorMessage(SCI_GETCHARAT, pos-1, (LPARAM)0);
	last[1] = 0;
	// convert from ANSI to wide byte
	wchar_t widechar[2];
	int success = MultiByteToWideChar(
		CP_ACP,			// code page
		0,				// flags (must be set to 0 for UTF8)
		last,			// in
		2,				// size of in (-1 means null-terminated)
		widechar,		// out
		2				// size of out (0 means calculate required size, but do not convert)
	);
	if (success == 0) {
		DWORD err = GetLastError();
		alert("Unable to convert from ANSI; unconverted char follows reason for failure");
		switch (err) {
			case ERROR_INSUFFICIENT_BUFFER:
				alert("Insufficient buffer");
				break;
			case ERROR_INVALID_FLAGS:
				alert("Invalid flags");
				break;
			case ERROR_INVALID_PARAMETER:
				alert("Invalid parameter");
				break;
			case ERROR_NO_UNICODE_TRANSLATION:
				alert("Invalid unicode");
				break;
			default:
				alert("Unknown error code: %d", GetLastError());
				break;
		}
		alert(last);
	}
	// convert to utf8
	return wide_2_utf8(widechar, 2);
}

char* PerlTextEditorEventSink::GetLastUtf8CharAsUtf8(int pos) {
	Sci_TextRange tr;
	tr.chrg.cpMax = pos;
	tr.chrg.cpMin = pos-1;
	tr.lpstrText = (char*)malloc(UTF8_MAXBYTES+1);
	unsigned char cur = m_doc->SendEditorMessage(SCI_GETCHARAT, tr.chrg.cpMin, (LPARAM)0);
	// if we have a multi-byte sequence, get it
	// (otherwise the range is already good)
	if (cur >= 0x80) {
		// keep looking until we find a head byte
		while (cur < 0xc0) {
			tr.chrg.cpMin--;
			cur = m_doc->SendEditorMessage(SCI_GETCHARAT, tr.chrg.cpMin, (LPARAM)0);
		}
	}
	m_doc->SendEditorMessage(SCI_GETTEXTRANGE, 0, (LPARAM)&tr);
	return tr.lpstrText;
}

// COMPLETELY UNTESTED
char* PerlTextEditorEventSink::GetLastDBCSCharAsUtf8(int pos, int cp) {
Perl_warn_nocontext("DBCS character fuctionality has not been tested");
	char* utf8 = (char*)malloc(UTF8_MAXBYTES+1);
	char last[4];
	last[0] = m_doc->SendEditorMessage(SCI_GETCHARAT, pos-2, (LPARAM)0);
	last[1] = m_doc->SendEditorMessage(SCI_GETCHARAT, pos-1, (LPARAM)0);
	last[2] = 0;
	last[3] = 0;

	int length = WideCharToMultiByte(
		cp,					// code page
		0,					// flags (must be set to 0 or WC_ERR_INVALID_CHARS)
		(wchar_t*)&last,	// in
		-1,					// size of in (-1 means null-terminated)
		utf8,				// out
		0,					// size of out (0 means calculate required size, but do not convert)
		NULL,				// which default character to use
		NULL				// upon return, indicates whether the default character was used
	);
	if (length == 0) {
		DWORD err = GetLastError();
		alert("Unable to convert double-byte encoding; unconverted sequence follows reason for failure");
		switch (err) {
			case ERROR_INSUFFICIENT_BUFFER:
				alert("Insufficient buffer");
				break;
			case ERROR_INVALID_FLAGS:
				alert("Invalid flags");
				break;
			case ERROR_INVALID_PARAMETER:
				alert("Invalid parameter");
				break;
			case ERROR_NO_UNICODE_TRANSLATION:
				alert("Invalid unicode");
				break;
			default:
				alert("Unknown error code: %d", GetLastError());
				break;
		}
		alert(last);
	}
	int success = WideCharToMultiByte(
		cp,					// code page
		0,					// flags (must be set to 0 or WC_ERR_INVALID_CHARS)
		(wchar_t*)&last,	// in
		-1,					// size of in (-1 means null-terminated)
		utf8,				// out
		length,				// size of out (0 means calculate required size, but do not convert)
		NULL,				// which default character to use
		NULL				// upon return, indicates whether the default character was used
	);
	if (success == 0) {
		DWORD err = GetLastError();
		alert("Unable to convert double-byte encoding; unconverted sequence follows reason for failure");
		switch (err) {
			case ERROR_INSUFFICIENT_BUFFER:
				alert("Insufficient buffer");
				break;
			case ERROR_INVALID_FLAGS:
				alert("Invalid flags");
				break;
			case ERROR_INVALID_PARAMETER:
				alert("Invalid parameter");
				break;
			case ERROR_NO_UNICODE_TRANSLATION:
				alert("Invalid unicode");
				break;
			default:
				alert("Unknown error code: %d", GetLastError());
				break;
		}
		alert((wchar_t*)&last);
	}
	return utf8;
}

char* PerlTextEditorEventSink::GetLastCharAsUtf8(int pos) {
	int cp = m_doc->SendEditorMessage(SCI_GETCODEPAGE, 0, (LPARAM)0);
	switch(cp) {
		case(0):
			return GetLastAnsiCharAsUtf8(pos);
			break;
		case(65001):
			return GetLastUtf8CharAsUtf8(pos);
			break;
		default:
			return GetLastDBCSCharAsUtf8(pos, cp);
	}
}

void PerlTextEditorEventSink::OnCharAdded(char c) {
	if (m_perl_doc == NULL)
		return;
	
	int pos = m_doc->SendEditorMessage(SCI_GETCURRENTPOS, 0, (LPARAM)0);
	char* utf8 = GetLastCharAsUtf8(pos);
	SV* sv_char = newSVpv(utf8, 0);	// 0 means Perl will calculate length
	// don't forget to free the memory from GetLastChar()
	free((void*)utf8);

	// call the event for every perl sink we're tracking
	for (int i = 0; i < m_perl_sinks.size(); i++) {
		dSP;

		ENTER;
		SAVETMPS;

		PUSHMARK(SP);
		XPUSHs(m_perl_sinks[i]);
		XPUSHs(m_perl_doc);
		XPUSHs(sv_char);
		PUTBACK;
	
		call_method("OnCharAdded", G_VOID|G_EVAL);
		SPAGAIN;

		if(SvTRUE(ERRSV)) {
			STRLEN n_a;
			alert("Error calling Perl version of OnCharAdded: %s\n", SvPV(ERRSV,n_a));
			POPs;
		}
	
		PUTBACK;
		FREETMPS;
		LEAVE;
	}
	SvREFCNT_dec(sv_char);	// don't need this any more

	return;
}

/*
 * PerlDocumentTracker
 */

PerlDocumentTracker::PerlDocumentTracker() {}

PerlDocumentTracker::~PerlDocumentTracker() {
	for (int i = 0; i < m_tracked_documents.size(); i++) {
		SvREFCNT_dec(m_tracked_documents[i]->perl_doc);
		delete m_tracked_documents[i];
	}
}

// add the menu; return true if added, false if already present
bool PerlDocumentTracker::Add(extensions::IDocumentPtr doc, TrackedDocument** tdoc) {
	// create a Perl object; we'll need on in either case
	extensions::IDocumentPtr* doc_ptr = new extensions::IDocumentPtr;
	*doc_ptr = doc;
	SV* perl_doc = create_perl_object((void*)doc_ptr, "PlPN::Document");

	// are we already being tracked?
	// if so, replace the Perl doc and return false
	*tdoc = Fetch(doc);
	if (*tdoc != NULL) {
		SvREFCNT_dec((*tdoc)->perl_doc);
		(*tdoc)->perl_doc = perl_doc;
		return false;
	}

	// not being tracked yet, create the TrackedDocument
	*tdoc = new TrackedDocument;
	(*tdoc)->cpp_doc = doc;
	(*tdoc)->perl_doc = perl_doc;

	(*tdoc)->document_sink = PerlDocumentEventSinkPtr(new PerlDocumentEventSink(doc, perl_doc));
	doc->AddEventSink((extensions::IDocumentEventSinkPtr)(*tdoc)->document_sink);

	(*tdoc)->text_editor_sink = PerlTextEditorEventSinkPtr(new PerlTextEditorEventSink(doc, perl_doc));
	doc->AddEventSink((extensions::ITextEditorEventSinkPtr)(*tdoc)->text_editor_sink);

	m_tracked_documents.push_back(*tdoc);
	return true;
}

TrackedDocument* PerlDocumentTracker::Fetch(extensions::IDocumentPtr doc) {
	for (int i = 0; i < m_tracked_documents.size(); i++) {
		if (m_tracked_documents[i]->cpp_doc == doc) {
			return m_tracked_documents[i];
		}
	}
	return NULL;
}

void PerlDocumentTracker::Remove(extensions::IDocumentPtr doc) {
	for (int i = 0; i < m_tracked_documents.size(); i++) {
		if (m_tracked_documents[i]->cpp_doc == doc) {
			// get rid of Perl doc
			if (m_tracked_documents[i]->perl_doc != NULL)
				SvREFCNT_dec(m_tracked_documents[i]->perl_doc);

			// remove sinks
			//doc->RemoveEventSink((extensions::IDocumentEventSinkPtr)m_tracked_documents[i]->document_sink);
			//doc->RemoveEventSink((extensions::ITextEditorEventSinkPtr)m_tracked_documents[i]->text_editor_sink);
			
			// get rid of the TrackedDocument itself
			delete m_tracked_documents[i];
			m_tracked_documents.erase(m_tracked_documents.begin()+i);
			break;
		}
	}
}

void PerlDocumentTracker::Blank() {
	for (int i = 0; i < m_tracked_documents.size(); i++) {
		if (m_tracked_documents[i]->perl_doc != NULL) {
			// decrement refcount if not in global destruction
			if (! PL_dirty)
				SvREFCNT_dec(m_tracked_documents[i]->perl_doc);
			m_tracked_documents[i]->perl_doc = NULL;
			m_tracked_documents[i]->document_sink->ResetPerlDoc(NULL);
			m_tracked_documents[i]->text_editor_sink->ResetPerlDoc(NULL);
		}
	}
}

void PerlDocumentTracker::Reset() {
	for (int i = 0; i < m_tracked_documents.size(); i++) {
		// decrement refcount if not in global destruction
		if (m_tracked_documents[i]->perl_doc != NULL && ! PL_dirty)
			SvREFCNT_dec(m_tracked_documents[i]->perl_doc);
		m_tracked_documents[i]->perl_doc = NULL;

		extensions::IDocumentPtr* doc_ptr = new extensions::IDocumentPtr;
		*doc_ptr = m_tracked_documents[i]->cpp_doc;
		m_tracked_documents[i]->perl_doc = create_perl_object((void*)doc_ptr, "PlPN::Document");
		m_tracked_documents[i]->document_sink->ResetPerlDoc(m_tracked_documents[i]->perl_doc);
		m_tracked_documents[i]->text_editor_sink->ResetPerlDoc(m_tracked_documents[i]->perl_doc);
	}
}

AV* PerlDocumentTracker::PerlArray() {
	AV* av = newAV();
	for (int i = 0; i < m_tracked_documents.size(); i++) {
		SvREFCNT_inc(m_tracked_documents[i]->perl_doc);
		av_store(
			av,									/* target */
			i,									/* index */
			m_tracked_documents[i]->perl_doc	/* is this fetch part of a store? */
		);
	}
	return av;
}

int PerlDocumentTracker::Count() {
	return (int)m_tracked_documents.size();
}
